/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 *    Apply.java
 *    Copyright (C) 2008-2012 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.core.pmml;

import java.util.ArrayList;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import weka.core.Attribute;

/**
 * Class encapsulating an Apply Expression.
 * 
 * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
 * @version $Revision 1.0 $
 */
class Apply extends Expression {

    /**
     * For serialization
     */
    private static final long serialVersionUID = -2790648331300695083L;

    /** The list of arguments the function encapsulated in this Apply Expression */
    protected ArrayList<Expression> m_arguments = new ArrayList<Expression>();

    /** The function to apply (either built-in or a DefineFunction) */
    protected Function m_function = null;

    /** The structure of the result of Apply Expression */
    protected Attribute m_outputStructure = null;

    /**
     * Constructor. Reads the function name and argument Expressions for this Apply
     * Expression.
     * 
     * @param apply     the Element encapsulating this Apply
     * @param opType    the optype for this expression (taken from either the
     *                  enclosing DefineFunction or DerivedField)
     * @param fieldDefs an ArrayList of Attributes for the fields that this
     *                  Expression might need to access
     * @param transDict the TransformationDictionary (may be null if there is no
     *                  dictionary)
     * @throws Exception if there is a problem parsing this Apply Expression
     */
    protected Apply(Element apply, FieldMetaInfo.Optype opType, ArrayList<Attribute> fieldDefs, TransformationDictionary transDict) throws Exception {
        super(opType, fieldDefs);

        String functionName = apply.getAttribute("function");
        if (functionName == null || functionName.length() == 0) {
            // try the attribute "name" - a sample file produced by MARS
            // uses this attribute name rather than "function" as defined
            // in the PMML spec
            functionName = apply.getAttribute("name");
        }

        if (functionName == null || functionName.length() == 0) {
            throw new Exception("[Apply] No function name specified!!");
        }
        // System.err.println(" *** " + functionName);
        m_function = Function.getFunction(functionName, transDict);

        // now read the arguments
        NodeList children = apply.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node child = children.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                String tagName = ((Element) child).getTagName();
                if (!tagName.equals("Extension")) {
                    // System.err.println(" ++ " + tagName);
                    Expression tempExpression = Expression.getExpression(tagName, child, m_opType, m_fieldDefs, transDict);
                    if (tempExpression != null) {
                        m_arguments.add(tempExpression);
                    }
                }
            }
        }

        if (fieldDefs != null) {
            updateDefsForArgumentsAndFunction();
        }
    }

    public void setFieldDefs(ArrayList<Attribute> fieldDefs) throws Exception {
        super.setFieldDefs(fieldDefs);
        updateDefsForArgumentsAndFunction();
    }

    private void updateDefsForArgumentsAndFunction() throws Exception {
        for (int i = 0; i < m_arguments.size(); i++) {
            m_arguments.get(i).setFieldDefs(m_fieldDefs);
        }

        // set the parameter defs for the function here so that we can determine
        // the structure of the output we produce
        ArrayList<Attribute> functionFieldDefs = new ArrayList<Attribute>(m_arguments.size());
        for (int i = 0; i < m_arguments.size(); i++) {
            functionFieldDefs.add(m_arguments.get(i).getOutputDef());
        }
        m_function.setParameterDefs(functionFieldDefs);
        m_outputStructure = m_function.getOutputDef();
    }

    /**
     * Get the result of evaluating the expression. In the case of a continuous
     * optype, a real number is returned; in the case of a categorical/ordinal
     * optype, the index of the nominal value is returned as a double.
     * 
     * @param incoming the incoming parameter values
     * @return the result of evaluating the expression
     * @throws Exception if there is a problem computing the result
     */
    public double getResult(double[] incoming) throws Exception {

        // assemble incoming to apply function to by processing each Expression
        // in the list of arguments
        double[] functionIncoming = new double[m_arguments.size()];
        // ArrayList<Attribute> functionParamTypes = m_function.getParameters();
        for (int i = 0; i < m_arguments.size(); i++) {
            functionIncoming[i] = m_arguments.get(i).getResult(incoming);
        }

        double result = m_function.getResult(functionIncoming);

        return result;
    }

    /**
     * Get the result of evaluating the expression for continuous optype. Is the
     * same as calling getResult() when the optype is continuous.
     * 
     * @param incoming the incoming parameter values mining schema
     * @return the result of evaluating the expression.
     * @throws Exception if the optype is not continuous.
     */
    public String getResultCategorical(double[] incoming) throws Exception {

        if (m_opType == FieldMetaInfo.Optype.CONTINUOUS) {
            throw new IllegalArgumentException("[Apply] Can't return result as " + "categorical/ordinal because optype is continuous!");
        }

        double result = getResult(incoming);
        return m_outputStructure.value((int) result);
    }

    /**
     * Return the structure of the result of applying this Expression as an
     * Attribute.
     * 
     * @return the structure of the result of applying this Expression as an
     *         Attribute.
     */
    public Attribute getOutputDef() {
        if (m_outputStructure == null) {
            // return a "default" output def. This will get replaced
            // by a final one when the final field defs are are set
            // for all expressions after all derived fields are collected
            return (m_opType == FieldMetaInfo.Optype.CATEGORICAL || m_opType == FieldMetaInfo.Optype.ORDINAL) ? new Attribute("Placeholder", new ArrayList<String>()) : new Attribute("Placeholder");
        }
        return m_outputStructure;// .copy(attName);
    }

    public String toString(String pad) {
        StringBuffer buff = new StringBuffer();

        // Used for DefineFunctions so that we can see which arguments
        // correspond to which parameters
        String[] parameterNames = null;

        buff.append(pad + "Apply [" + m_function.toString() + "]:\n");
        buff.append(pad + "args:");
        if (m_function instanceof DefineFunction) {
            parameterNames = m_function.getParameterNames();
        }
        for (int i = 0; i < m_arguments.size(); i++) {
            Expression e = m_arguments.get(i);
            buff.append("\n" + ((parameterNames != null) ? pad + parameterNames[i] + " = " : "") + e.toString(pad + "  "));
        }

        return buff.toString();
    }
}
